#!/usr/bin/python

"""
Pacmon, an updates notification tray application for the users of Arch Linux
Copyright 2006 Matt McDonald <metzen@gmail.com>

This file is part of Pacmon.

Pacmon is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

Pacmon is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""

import os
import sys
import re
import urllib
import tarfile

from ConfigParser import *

import mx.Tools

class PacmanConfig(object):
    def __init__(self, repositories, hold_pkgs, ignore_pkgs):
        self.repositories = repositories
        self.hold_pkgs = hold_pkgs
        self.ignore_pkgs = ignore_pkgs

class PacmanConfigParser(ConfigParser):
    """Parses a Pacman configuration file.  It is similar to a normal
    ini/config file, except that an option can be repeated, and the values
    will be placed into a list. For example:

      [extra]
      Server = foo1
      Server = foo2

    In a normal config file, in the section 'extra', 'Server' = 'foo2'
    However, in the Pacman configuration, 'Server' = ['foo1', 'foo2']

    Additionally, the Pacman config file can contain multiple values on the same
    line, separated by spaces.  For example:

      [options]
      NoUpgrade = etc/passwd etc/group etc/shadow etc/sudoers
    """

    def _read(self, fp, fpname):
        """Parse a sectioned setup file.

        The sections in setup file contains a title line at the top,
        indicated by a name in square brackets (`[]'), plus key/value
        options lines, indicated by `name: value' format lines.
        Continuations are represented by an embedded newline then
        leading whitespace.  Blank lines, lines beginning with a '#',
        and just about everything else are ignored.
        """
        cursect = None                            # None, or a dictionary
        optname = None
        lineno = 0
        e = None                                  # None, or an exception
        while True:
            line = fp.readline()
            if not line:
                break
            lineno = lineno + 1
            # comment or blank line?
            if line.strip() == '' or line[0] in '#;':
                continue
            if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR":
                # no leading whitespace
                continue
            # continuation line?
            if line[0].isspace() and cursect is not None and optname:
                value = line.strip()
                if value:
                    cursect[optname] = "%s\n%s" % (cursect[optname], value)
            # a section header or option header?
            else:
                # is it a section header?
                mo = self.SECTCRE.match(line)
                if mo:
                    sectname = mo.group('header')
                    if sectname in self._sections:
                        cursect = self._sections[sectname]
                    elif sectname == DEFAULTSECT:
                        cursect = self._defaults
                    else:
                        cursect = {'__name__': sectname}
                        self._sections[sectname] = cursect
                    # So sections can't start with a continuation line
                    optname = None
                # no section header in the file?
                elif cursect is None:
                    raise MissingSectionHeaderError(fpname, lineno, line)
                # an option line?
                else:
                    mo = self.OPTCRE.match(line)
                    if mo:
                        optname, vi, optval = mo.group('option', 'vi', 'value')
                        if vi in ('=', ':') and ';' in optval:
                            # ';' is a comment delimiter only if it follows
                            # a spacing character
                            pos = optval.find(';')
                            if pos != -1 and optval[pos-1].isspace():
                                optval = optval[:pos]
                        optval = optval.strip()
                        # allow empty values
                        if optval == '""':
                            optval = ''
                        optname = self.optionxform(optname.rstrip())
                        #-----PACMAN CONF HACKING HERE-------------------------
                        try:
                            cursect[optname] += optval.split()
                        except KeyError:
                            # optname is not in dictionary yet
                            cursect[optname] = optval.split()
                        #------------------------------------------------------
                    else:
                        # a non-fatal parsing error occurred.  set up the
                        # exception but keep going. the exception will be
                        # raised at the end of the file and will contain a
                        # list of all bogus lines
                        if not e:
                            e = ParsingError(fpname)
                        e.append(lineno, repr(line))
        # if any parsing errors occurred, raise an exception
        if e:
            raise e

def parse_conf(config_path):
    """Open the pacman config file at 'config_path' and reads the config
    information.

    Returns a PacmanConfig
    """
    parser = PacmanConfigParser()
    parser.read(config_path)
    # parse all the "Include =" files
    for section in parser.sections():
        try:
            inc = parser.get(section, 'include')
            parser.read(inc)
        except:
            pass # no Include option
    repos, hold_pkgs, ignore_pkgs = [], [], []
    try: hold_pkgs = parser.get('options', 'holdpkg')
    except: pass

    try: ignore_pkgs = parser.get('options', 'ignorepkg')
    except: pass

    for section in parser.sections():
        if not section == 'options':
            repos += [(section, parser.get(section, 'Server')[0])]
    return PacmanConfig(repos, hold_pkgs, ignore_pkgs)

def get_installed_pkgs():
    """Call 'pacman -Q' to get a list of tuples containing all the currently
    installed packages and their versions.
    [(pkg1, ver1), (pkg2, ver2)]
    """
    pacmanQ = os.popen('pacman -Q')
    pacmanQout = pacmanQ.read().strip()
    # parse the packages and their versions: pk1 5.4.2-1
    reg = re.compile('(.+) (.+-.+)')
    return [ reg.match(item).groups() for item in pacmanQout.split('\n') ]
    #return pacmanQout.split('\n')[:-1] #remove trailing whitespace


progress = 0 # used to indicate the percent of progress completed to a caller

def get_updates_list(output_buffer=sys.stdout):
    """Return a list of tuples containing (name, curr-ver, new-ver) for each
    package that requires an update"""
    globals()['progress'] = 0

    pacman_config = parse_conf('/etc/pacman.conf')

    servers = pacman_config.repositories
    installed_pkgs = get_installed_pkgs()

    #build the dictionary of currently installed packages
    pkgDict = dict(installed_pkgs)


    #build the dictionary of all packages residing on servers
    srvList = []
    p = re.compile('(.*)-(.*-.*)/')

    #set progessbar to 10%
    globals()['progress'] = .1


    for server in servers:
        servName = server[0]
        servURL = server[1]
        output_buffer.write('Checking ' + servName + '...')
        output_buffer.flush()

        try:
            # each repository will have a file called '<reponame>.db.tar.gz' in
            # its root dir.  It contains a listing of all the packages on the
            # repo, and their respective versions
            serverDB = urllib.urlopen(servURL + '/' + servName + '.db.tar.gz')
            serverDBc = tarfile.open(mode="r|gz", fileobj=serverDB)
            for taritem in serverDBc:
                if taritem.isdir():
                    srvList += [ p.match(taritem.name).groups() ]
            output_buffer.write("done\n")

            globals()['progress'] = progress + (1 - progress) / 6

        except IOError:
            print "Connection error!!\n---> Failed to download:",
            print servURL + '/' + servName + '.db.tar.gz\n'

    # build the serverpkg dictionary
    srvList.sort()
    srvDict = dict(srvList)

    #set progress bar to 90%
    globals()['progress'] = .9

    # build the list of packages to update
    updatesList = []
    for package in pkgDict.items():
        pkgName = package[0]
        pkgVer, pkgRel = package[1].split('-')

        # check to see if we should ignore upgrading this pkg
        if pacman_config.ignore_pkgs.count(pkgName) > 0:
            continue

        if srvDict.has_key(pkgName):

            # separate the version number and the release num (0.5.2-2)
            srvVer, srvRel = srvDict[pkgName].split('-')

            # perform accurate comparison between version numbers
            compare = mx.Tools.verscmp(srvVer, pkgVer)
            SRV_NEWER = 1
            SAME = 0
            PKG_NEWER = -1

            # we need to add this package to the updates list if:
            #   the server version number is newer, OR
            #   the two versions are the same, but server has a newer rel-number
            if compare == SRV_NEWER or (compare == SAME and int(srvRel) > int(pkgRel)):
                updatesList += [(pkgName,pkgVer+'-'+pkgRel, srvVer+'-'+srvRel)]

    #set progress bar to 100%
    globals()['progress'] = 1

    # write out the list of updates to the message buffer if we have any
    if len(updatesList) > 0:
        output_buffer.write('\n-----Updates-----\n')
        for pkg in updatesList:
            output_buffer.write(pkg[0] + '-' + pkg[1] + ' => ' + pkg[2] + '\n')

    return updatesList

def get_progress():
    return progress




